iT邦幫忙

2024 iThome 鐵人賽

DAY 18
0
JavaScript

Don't make JavaScript Just Surpise系列 第 18

生成器(Generator)

  • 分享至 

  • xImage
  •  

昨天講完迭代器後,今天要來討論另一種也是 ES 6 引入,且基於迭代器的實作標準:生成器(Generator)。

Generator(ES 6)

Generator 是 ES 6 引入的一種物件實作標準,透過 generator function 返回。
實際上 Generator 是基於迭代器規範實作的子類別,為什麼這樣說,我們來看看實際宣告的例子。

我們可以通過 function* 這樣來宣告一個 generator function

function* demoGenerator() {
    console.log('Start');
    yield 'foo';
    yield 'bar';
    console.log('End');
}

const gen = demoGenerator();
// "Start"
for(let val of gen){
	console.log(`val : ${val}`);// val : "foo" | val : "bar"
}
console.log(gen.next()); // { value: 'foo', done: false }
console.log(gen.next()); // { value: 'bar', done: false }
// "End"
console.log(gen.next()); // { value: undefined, done: true }

可以看到昨天提到的 next() 函式在這裡也出現了,而且 gen 物件也可以透過 for of 來遍歷,所以建構生成器時返回了一個可迭代物件(iterable object),以 yield 來實現迭代器的相關訪問行為。
某種意義上可以說生成器就是一個特別應用情景的迭代器。

generator function 中,我們可以使用 yield 關鍵字來「暫停」函式的執行。
如上述例子,利用 demoGenerator() 建構了一個 genGenerator 物件。

如果他是一個一般的函式,應該會在印完 "Start" 後立刻印出 "End",但上面的例子中,他透過 yield 暫停了函式的執行,直到對 Generator 物件調用了 .next() 才返回了 yield 關鍵字指定回傳的值,而 for of函式也僅針對 yield 的回傳值做顯示。

生成器操作的概念上有點像狀態機,每個 yield 關鍵字的回傳就像一個狀態,透過 .next() 的呼叫來做狀態的轉移。

拋出例外或錯誤(throw exception)

生成器內部可以透過 throw 語句來拋出錯誤,一旦錯誤被拋出,有兩種處理情形:

  1. 例外被生成器內部的 try catch 接住了,則能夠繼續進行後續的 yield
  2. 例外沒有被接住,則生成器的迭代視為結束,無法接續調用 next()
function* demoGenerator() {
    console.log('Start');
      for(let i = 0; i < 4; i++){
        try{
          if(i === 1) throw Error("count to three");
          else yield i;
         }
         catch(error){
            console.log(`caught in generator : ${error.message}`);

        }
    }
		throw Error("count to four");
		yield 5;
    console.log('End');
}

const gen = demoGenerator();
// "Start"
for(let val of gen){
	console.log(`val : ${val}`);  
}
//"val : 0"
//"caught in generator : count to three"
//"val : 2"
//"val : 3"
//"<a class='gotoLine' href='#59:9'>59:9</a> Uncaught Error: count to four"

yield* 語法糖

yield* 和一般的 yield 差後面多了一個 *,記得上面 function 加了一個 * 差在哪裡嗎?
沒錯,他會返回一個生成器物件。

function* genA() {
    yield 1;
    yield 2;
}

let arr = [4,5];

function* genB() {
    yield* genA();
    yield 3;
    yield* arr;
}

const gen = genB();
for(let val of gen){
	console.log(val);// 1 2 3 4 5
}

除了生成器以外,yield* 語法也能夠對可迭代物件使用(例子中的 arr)。
這樣的語法簡化了本來如果要訪問一個可迭代物件內容做 yield 時使用 for of 的情景。
當多個生成器有可能以不同的順序互相組合時,這種語法也簡化了程式的複雜度,提高程式碼的可重用性。

透過生成器迭代物件

昨天我們有說過一般的物件並非一個可迭代的對象,但如果我們配合使用生成器,我們就可以把物件變得可迭代。

let obj = {foo:'foo', bar:'bar'}
function* demoGenerator(obj) {
  for (let key of Object.keys(obj)) {
    yield [key, obj[key]];
  }
}
for(let val of demoGenerator(obj)){
    console.log(val);//"foo" "bar"
}

這樣一個簡單的生成器可以泛用於轉換一般的物件變為可迭代,就可以對做出來的迭代器物件使用那些迭代器標準上的方法。

生成器的雙向互動

在迭代器裡,next()方法僅用於按照固定的順序返回值,無法接受參數傳入。
而生成器裡面,next()方法是可以接收傳入值的,一旦 next() 收到傳入值,便可以在生成器內部做對應的邏輯操作。

function* orderMaker() {
	  let order = yield "請輸入您想訂購的餐點";
    yield `${order} 正在準備中`;
    yield `您的 ${order} 已準備完成`;
    return "感謝您的光臨"
}

let coolOrderMaker = orderMaker();
console.log(coolOrderMaker.next().value); //請輸入您想訂購的餐點
console.log(coolOrderMaker.next("咖哩飯").value); //咖哩飯 正在準備中
console.log(coolOrderMaker.next().value); // 您的 咖哩飯 已準備完成
console.log(coolOrderMaker.next()); // { value: "感謝您的光臨", done: true }
console.log(coolOrderMaker.next()); // { value: undefined, done: true }

注意賦值時機點發生在宣告賦值後下一次的 next() 呼叫。
如第一次呼叫拿到詢問,下一次呼叫時 next("咖哩飯") 存入的值會存到 order 裡。

附帶一提,如果有寫 return 語句,可以用於存到第一次觸碰到 done: true 時的 value。當然,後續繼續嘗試用 next() 的話 value 就會變回 undefined 了,僅有第一次會有值。

生成器的重要性

生成器推出後主要的改變:

  1. 函式變得可以記憶狀態,暫停執行(以前的函式執行就會一路執行到底)
  2. 是一種更便於使用的迭代器宣告形式,使用 yield 來指定每次迭代的回傳內容,也因為可迭代,在使用 for of 等迭代器語法也相當方便
  3. yield* 語法糖對複雜結構的函式流程控制、使用都更加方便且易於閱讀
  4. 可以透過 next() 接收外部數據來對生成器內部做到動態的邏輯改變

迭代器本身僅提供一個遍歷集合內容的方法,我們可以說生成器是一種更為強大的迭代器,更適合用於複雜邏輯的處理。

生成器與異步

在更後面的語法出來之前,PromiseGenerator 曾經常常被一起使用:能夠暫停函式的行為,對需要等待不確定時間的遠端呼叫再適合不過。
比如有個函式庫叫做 co,他的實踐就是透過生成器為基礎,程式碼並不多,只要看這個 檔案 就行。

展示一段使用 co 的程式碼:

function fetchData(url) {
    return new Promise((resolve) => {
        setTimeout(() => {
            resolve(`Get data from ${url}`);
        }, 1000);
    });
}
         
co(function* () {
    const result1 = yield fetchData('https://api.example.com/data1');
    const result2 = yield fetchData('https://api.example.com/data2');
}).catch(err => {
    console.error('發生錯誤:', err);
    document.getElementById('output').innerHTML = '<p>發生錯誤,請查看控制台。</p>';
});

co 允許了使用者在生成器中呼叫異步程式碼,並使用 yield 來等待其結果。
這樣就能夠保證呼叫的順序,簡化了異步的管理和避免回呼地獄,使得異步處理上更接近同步的程式碼風格。

在後續的語法出來前,Generator 加上 Promise 是那段時間裡處理異步常用的方式。


上一篇
ES 6 的新資料結構與迭代器(Iterator)
下一篇
async 和 await 關鍵字
系列文
Don't make JavaScript Just Surpise31
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言